iT邦幫忙

2022 iThome 鐵人賽

DAY 28
0
Software Development

Kotlin on the way系列 第 28

Day 28 設計模式 狀態模式和狀態機

  • 分享至 

  • xImage
  •  
  • state
  • state pattern vs stragety pattern
  • state machine
  • redux state machine
  • coroutine state machine
  • resource

狀態 State

狀態模式是透過將行為封裝在狀態之中,Context 的行為會委派給一個物件,而狀態改變,Context 的行為也會改變,但是使用Context 的客戶並不會知道狀態變化,也不知道行為有變

舉個例子吧,美式軟餅乾!!! 下面是真實食譜XD

typealias pcs = Int

abstract class SoftCookieRecipe {
    private val sodaPowder = 3
    private val salt = 5

    protected val flour = 280 + sodaPowder + salt
    protected val butter = 250
    protected val sugar = 140
    protected val brownSugar = 140
    protected val vanillaExtra = 10
    protected val chocolateBean = 225
    protected val egg: pcs = 2

    abstract fun makeCookie()
    fun mix(vararg t: Int): Int {
        return t.sum()
    }

    fun bake(chocolateDough:Int){
        // temperature 170
    }
}

class StandardRecipe : SoftCookieRecipe() {
    override fun makeCookie() {
        val solidButterWithSugar = mix(butter, sugar)
        val eggCream = mix(egg,solidButterWithSugar, vanillaExtra)
        val dough = mix(eggCream,flour)
        val chocolateDough = mix(dough,chocolateBean)
    }

}

class CustomRecipe : SoftCookieRecipe() {
    private val iceCube = butter * 13 / 100

    override fun makeCookie() {
        val brownButter = brownTheButter(butter)
        val eggCream = whippedEgg(sugar)
        val brownButterWithSugar = mix(brownButter, brownSugar)
        val batter = mix(brownButterWithSugar, eggCream, flour)
        val chocolateBatter = mix(batter, chocolateBean)
        val chocolateDough = chocolateBatter.moveToRefrigerator()
    }


    private fun whippedEgg(item: Int): Int {
        return item
    }

    private fun brownTheButter(item: Int): Int {
        //start remove 13% water

        //add equal weight to 13% water ice cube to cool down
        return item + iceCube
    }

}

private fun Int.moveToRefrigerator():Int {
    Thread.sleep(180000)
    return this
}

好的,上面有兩種食譜,一種是傳統食譜,另一種是特製軟餅乾食譜,但是兩者之間很明顯製作方式不同,特製軟食譜醒的時間長,麵粉部分會從複雜澱粉變成單醣類,而且糖分別加在蛋液和奶油液體裡面,而麵團再加熱後第一時間會是往外攤,液體糖漿相比固體糖粒的面積大,也就讓特製軟餅乾會有更多焦化外殼,以及更鬆軟的內餡

那呼叫方呢?烘焙師可以會有製作餅乾的方法,一天做經典口味,一天做客制口味,每天晚上要備隔天的料

class WorkContext(var state: State) {

    fun switch() {
        state.jobLog(this)
    }
}

abstract class State {

    abstract fun jobLog(ctx: WorkContext)
}

class ClassicWorkLog : State() {

    override fun jobLog(ctx: WorkContext) {
        StandardRecipe().makeCookie()
        ctx.state = CusotmWorkLog()//每天的最後更改狀態
    }
}

class CusotmWorkLog:State() {
     
     override fun jobLog(ctx: WorkContext) {
        CustomRecipe().makeCookie()
        ctx.state = ClassicWorkLog()//每天的最後更改狀態
    }
}

之後烘焙師就殷勤的每天上工

fun main(){
    val workStateContext =  WorkContext(ClassicWorkLog())
    while(true) {
        workStateContext.jobLog()//一天工作結束後就會切換狀態拉
    }
}

狀態模式優點在於將狀態的變化封裝起來,利用不同狀態分割行為,使得新增新的行為變得容易,而外部調用者甚至不需關注狀態如何變化,也極大程度的簡化了 if/ else 或是 switch 判斷

state pattern vs stragety pattern

狀態模式和策略模式其實很像,他的差異在狀態模式封裝了狀態的變化,而策略模式則是由外部調用指定,那將上面的程式碼轉換成策略模式就會變成

fun main(){
    var isClassicDay = true
    while(true){
        if(isClassicDay) StandardRecipe().makeCookie()
        else CustomRecipe().makeCookie()
        
        isClassicDay = !isClassicDay
    }
}

state machine

狀態機是一種狀態模式的進階實現,可能會隨著狀態的增加而有不同的實現,現實中最簡單的有限狀態機就是門


門會有開著跟關著兩種狀態,你沒辦法關一個已經關上的門

通常在實現狀態機時,也會又個狀態變化圖供工程師參考,再舉個交友軟體的範例

用戶在左滑右滑會分為 like, dislike,而付費用戶可以反悔他的 dislike ,把拒絕過的候選人拉回候選池,而如果雙方都 like 對方,就可以配對成功

fun main() {
   val match = MatchEntity()
   
   //tryFail(match)
   tryMatch(match)
}

fun tryFail(match:MatchEntity){
   match.like() //pending
   match.unMatch() //unmatch
}
fun tryMatch(match:MatchEntity){
   match.dislike() //failed
   match.rewindToLiked() //pending
   match.match() //matched
}

enum class MatchState {
    Created,
    Pending,
    Failed,
    Matched,
}

enum class Command {
    Create,
    Like,
    Dislike,
    RewindToCreated,
    RewindToLiked,
    RewindToDisliked,
    Match,
    UnMatch,
}

data class StateTransition(
    val currentState: MatchState, 
    val command: Command
)

class MatchEntity:MatchCommand{
    private var currentState = MatchState.Created
    private val transitionChecker = mapOf(
        StateTransition(MatchState.Created, Command.Like) to MatchState.Pending,
        StateTransition(MatchState.Created, Command.Dislike) to MatchState.Failed,
        StateTransition(MatchState.Pending, Command.Match) to MatchState.Matched,
        StateTransition(MatchState.Pending, Command.UnMatch) to MatchState.Failed,
        StateTransition(MatchState.Failed, Command.RewindToCreated) to MatchState.Created,
        StateTransition(MatchState.Failed, Command.RewindToLiked) to MatchState.Pending,
        StateTransition(MatchState.Failed, Command.RewindToDisliked) to MatchState.Failed,
    )

    private fun changeState(command: Command){
        val newTransition = StateTransition(currentState, command)
        transitionChecker[newTransition]?.let {
            currentState = it
        } ?: throw Exception("not support exception")
    }

    override fun like() {
        changeState(Command.Like)
        println(currentState.name)
    }

    override fun dislike() {
        changeState(Command.Dislike)
        println(currentState.name)

    }

    override fun match() {
        changeState(Command.Match)
        println(currentState.name)

    }

    override fun unMatch() {
        changeState(Command.UnMatch)
        println(currentState.name)

    }

    override fun rewindToCreated() {
        changeState(Command.RewindToCreated)
        println(currentState.name)

    }

    override fun rewindToLiked() {
        changeState(Command.RewindToLiked)
        println(currentState.name)

    }

    override fun rewindToDisliked() {
        changeState(Command.RewindToDisliked)
        println(currentState.name)

    }
}

interface MatchCommand{
    fun like()

    fun dislike()

    fun match()

    fun unMatch()

    fun rewindToCreated()

    fun rewindToLiked()

    fun rewindToDisliked()
}

我們透過預先定義所有確定存在狀態路徑,以呼叫指令的方式,由狀態機執行實際邏輯,在調用方只需送出指令即可切換狀態,而透過上面的範例我們可以歸納出一些特徵,狀態機會包含

  1. state,當前物件或畫面的狀態,根據狀態可能會有不同行為或畫面渲染
  2. event,用來觸發狀態變化的事件,可能由用戶互動,或是條件判斷去呼叫
  3. action,事件發生後要執行的動作,用來告知狀態機的行為
  4. transition,狀態機執行行為和狀態變化,並產生新狀態

redux state machine

既然對狀態機已經有初步了解,我們就可以看看 redux 狀態機模型

在 react/ redux 的狀態機模型中,就是以 ui 為狀態顯示和事件觸發的, state 渲染畫面,用戶互動產生 event 丟到 eventHandler 產出對應的 action 交由 reducer 執行狀態變化,並產生新的狀態去繪製 ui

誒,那 react 關 Kotlin 什麼事?
像我之前說過的,不要被架構綁架, Android 不是只有 MVVM 可以實現,架構本身可以獨立討論,也可以套進其他專案,但我在這邊提起的原因,不僅是因為講到狀態機,同時也因為 jetpack compose 便是使用狀態去做響應式畫面渲染,這個架構可以使用 MVVM, MVI, Redux,我們可以看看在開源專案中的案例

在 KMM 之中,ios and android,會有各自的 ui 實現,而他們會共用資料和商業邏輯,當然最重要的事,架構並不會限於某個語言或是框架,而要要選擇適合的去使用

coroutine state machine

而 coroutine 可以說是把狀態機玩得出神入化,在 coroutine 裡面,有一個關鍵字叫做 suspend ,中文叫掛起,那他的功能就是在不同執行序之間切換任務執行,其背後的原理就是狀態機,詳細的細節可以看Day 9 Kotlin coroutine 黑魔法 suspend

這邊也簡單提一下,掛起函示在編譯後,會變成

when(continuation.label) {
        0 -> {
            // Checks for failures
            throwOnFailure(continuation.result)
            // Next time this continuation is called, it should go to state 1
            continuation.label = 1
            // The continuation object is passed to logUserIn to resume 
            // this state machine's execution when it finishes
            userRemoteDataSource.logUserIn(userId!!, password!!, continuation)
        }
        1 -> {
            // Checks for failures
            throwOnFailure(continuation.result)
            // Gets the result of the previous state
            continuation.user = continuation.result as User
            // Next time this continuation is called, it should go to state 2
            continuation.label = 2
            // The continuation object is passed to logUserIn to resume 
            // this state machine's execution when it finishes
            userLocalDataSource.logUserIn(continuation.user, continuation)
        }
        2 -> {
            // Checks for failures
            throwOnFailure(continuation.result)
            // Gets the result of the previous state
            continuation.userDb = continuation.result as UserDb
            // Resumes the execution of the function that called this one
            continuation.cont.resume(continuation.userDb)
        }
        else -> throw IllegalStateException(...)

他會利用 continuation.label 去紀錄執行到哪邊
而最後執行的效果就會是

resource

圖源
如何有邏輯的釐清事物的狀態

範例源
States and State Machines

English

  • state
  • state pattern vs strategy pattern
  • state machine
  • redux state machine
  • coroutine state machine
  • resource

State

state pattern change behavior by encapsulate behavior within different state, the behavior of Context will interest to another object, when the state changed, the behavior of context also change, but the client of Context wouldn't know about it

Let's check out a real sample

typealias pcs = Int

abstract class SoftCookieRecipe {
    private val sodaPowder = 3
    private val salt = 5

    protected val flour = 280 + sodaPowder + salt
    protected val butter = 250
    protected val sugar = 140
    protected val brownSugar = 140
    protected val vanillaExtra = 10
    protected val chocolateBean = 225
    protected val egg: pcs = 2

    abstract fun makeCookie()
    fun mix(vararg t: Int): Int {
        return t.sum()
    }

    fun bake(chocolateDough:Int){
        // temperature 170
    }
}

class StandardRecipe : SoftCookieRecipe() {
    override fun makeCookie() {
        val solidButterWithSugar = mix(butter, sugar)
        val eggCream = mix(egg,solidButterWithSugar, vanillaExtra)
        val dough = mix(eggCream,flour)
        val chocolateDough = mix(dough,chocolateBean)
    }

}

class CustomRecipe : SoftCookieRecipe() {
    private val iceCube = butter * 13 / 100

    override fun makeCookie() {
        val brownButter = brownTheButter(butter)
        val eggCream = whippedEgg(sugar)
        val brownButterWithSugar = mix(brownButter, brownSugar)
        val batter = mix(brownButterWithSugar, eggCream, flour)
        val chocolateBatter = mix(batter, chocolateBean)
        val chocolateDough = chocolateBatter.moveToRefrigerator()
    }


    private fun whippedEgg(item: Int): Int {
        return item
    }

    private fun brownTheButter(item: Int): Int {
        //start remove 13% water

        //add equal weight to 13% water ice cube to cool down
        return item + iceCube
    }

}

private fun Int.moveToRefrigerator():Int {
    Thread.sleep(180000)
    return this
}

Alright, so we have two different kind of recipe, first one is tradition cookie, the other one is special soft cookie, although the ingredient is same, the process is different, the cook hour of special one is longer, allow the strach transfer into Monosaccharides(simple sugar), and the sugar is separate to mix with egg and liquid butter, when the dough met heat, it will become fluid for a while, the liquid syrup dough comes with bigger area of coking Shell and softer stuffing

What about the client?
bakery have the method the make cookie, one day tradition, one day special, every night he need to prepare for tomorrow

class WorkContext(var state: State) {

    fun switch() {
        state.jobLog(this)
    }
}

abstract class State {

    abstract fun jobLog(ctx: WorkContext)
}

class ClassicWorkLog : State() {

    override fun jobLog(ctx: WorkContext) {
        StandardRecipe().makeCookie()
        ctx.state = CusotmWorkLog()//change state in the end of day
    }
}

class CusotmWorkLog:State() {
     
     override fun jobLog(ctx: WorkContext) {
        CustomRecipe().makeCookie()
        ctx.state = ClassicWorkLog()//change state in the end of day
    }
}

Then the baker works hard everyday

fun main(){
    val workStateContext =  WorkContext(ClassicWorkLog())
    while(true) {
        workStateContext.jobLog()//change state
    }
}

The advantage of state pattern is it encapsulate the state change, using different state separate behavior, more flexible to add new behavior, and outer client don't need to care about how does the state change, simplemize the if, else, switch

state pattern vs strategy pattern

state pattern and strategy is similar, the different is state pattern encapsulate state change, but strategy pattern indicate by client, so the code above transfer into strategy pattern is

fun main(){
    var isClassicDay = true
    while(true){
        if(isClassicDay) StandardRecipe().makeCookie()
        else CustomRecipe().makeCookie()
        
        isClassicDay = !isClassicDay
    }
}

state machine

State machine is a advance implement of state pattern, could come out with different implement with new state added, in the real life, the simple limited state machine is door

state diagram
there are two state of door, closed and open

Check a sample with dating app

All user slip to right means like, left means dislike, and paid user can regret their dislike, add rejected candidate back to pool, if two person sent a like to each other, then they are matched

fun main() {
   val match = MatchEntity()
   
   //tryFail(match)
   tryMatch(match)
}

fun tryFail(match:MatchEntity){
   match.like() //pending
   match.unMatch() //unmatch
}
fun tryMatch(match:MatchEntity){
   match.dislike() //failed
   match.rewindToLiked() //pending
   match.match() //matched
}

enum class MatchState {
    Created,
    Pending,
    Failed,
    Matched,
}

enum class Command {
    Create,
    Like,
    Dislike,
    RewindToCreated,
    RewindToLiked,
    RewindToDisliked,
    Match,
    UnMatch,
}

data class StateTransition(
    val currentState: MatchState, 
    val command: Command
)

class MatchEntity:MatchCommand{
    private var currentState = MatchState.Created
    private val transitionChecker = mapOf(
        StateTransition(MatchState.Created, Command.Like) to MatchState.Pending,
        StateTransition(MatchState.Created, Command.Dislike) to MatchState.Failed,
        StateTransition(MatchState.Pending, Command.Match) to MatchState.Matched,
        StateTransition(MatchState.Pending, Command.UnMatch) to MatchState.Failed,
        StateTransition(MatchState.Failed, Command.RewindToCreated) to MatchState.Created,
        StateTransition(MatchState.Failed, Command.RewindToLiked) to MatchState.Pending,
        StateTransition(MatchState.Failed, Command.RewindToDisliked) to MatchState.Failed,
    )

    private fun changeState(command: Command){
        val newTransition = StateTransition(currentState, command)
        transitionChecker[newTransition]?.let {
            currentState = it
        } ?: throw Exception("not support exception")
    }

    override fun like() {
        changeState(Command.Like)
        println(currentState.name)
    }

    override fun dislike() {
        changeState(Command.Dislike)
        println(currentState.name)

    }

    override fun match() {
        changeState(Command.Match)
        println(currentState.name)

    }

    override fun unMatch() {
        changeState(Command.UnMatch)
        println(currentState.name)

    }

    override fun rewindToCreated() {
        changeState(Command.RewindToCreated)
        println(currentState.name)

    }

    override fun rewindToLiked() {
        changeState(Command.RewindToLiked)
        println(currentState.name)

    }

    override fun rewindToDisliked() {
        changeState(Command.RewindToDisliked)
        println(currentState.name)

    }
}

interface MatchCommand{
    fun like()

    fun dislike()

    fun match()

    fun unMatch()

    fun rewindToCreated()

    fun rewindToLiked()

    fun rewindToDisliked()
}

We define the state could happen, by calling command, use state machine operator logic, in the caller side, we only need to send command to change state, with this sample, we have following conclusion

  1. state, Context of class or UI, its behavior or render depend on state
  2. event, Event used to trigger state changed
  3. action, execute action after event happened, use to confirm state machine behavior
  4. transition, state machine execute action, state change, and generate new state

redux state machine

Now we have basic understand of state machine, we can check out redux state model

in react/ redux state machine, ui is used for display state and trigger event, render ui with state, and throw new event to eventHandler by user interaction, generate action and hand over reducer to execute state changed, using new state to render UI

So, what is the point with Kotlin?
As i said before, do blind your eyes by architecture, software doesn't only runs on one kind of architecture, the architecture could discuss alone, the reason I mentioned here, nit just because of state machine, but also the jetpack compose is render based on state, we can choose MVVM, MVI, Redux, check out an open source sample

In KMM, ios and Android has their own ui Implement, and they shared data and business logic, more important is choose the proper architecture for your use case

coroutine state machine

Inside Kotlin Coroutine they have great usage of state machine, there is a keyword call suspend, its function is switch task between different thread, under the hood it use state machine, the detail implement can check out hereDay 9 Kotlin coroutine 黑魔法 suspend

Quickly spoiler, suspend function will compiled into

when(continuation.label) {
        0 -> {
            // Checks for failures
            throwOnFailure(continuation.result)
            // Next time this continuation is called, it should go to state 1
            continuation.label = 1
            // The continuation object is passed to logUserIn to resume 
            // this state machine's execution when it finishes
            userRemoteDataSource.logUserIn(userId!!, password!!, continuation)
        }
        1 -> {
            // Checks for failures
            throwOnFailure(continuation.result)
            // Gets the result of the previous state
            continuation.user = continuation.result as User
            // Next time this continuation is called, it should go to state 2
            continuation.label = 2
            // The continuation object is passed to logUserIn to resume 
            // this state machine's execution when it finishes
            userLocalDataSource.logUserIn(continuation.user, continuation)
        }
        2 -> {
            // Checks for failures
            throwOnFailure(continuation.result)
            // Gets the result of the previous state
            continuation.userDb = continuation.result as UserDb
            // Resumes the execution of the function that called this one
            continuation.cont.resume(continuation.userDb)
        }
        else -> throw IllegalStateException(...)

By using continuation.label to record its state, how it behavior in code will look like

resource

image source
如何有邏輯的釐清事物的狀態

sample source
States and State Machines


上一篇
Day 27 設計模式 裝飾和代理的細節 Proxy pattern and Decorator pattern Structural
下一篇
Day 29 設計模式 依賴注入的細節
系列文
Kotlin on the way31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言